import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.List;
import java.util.ArrayList;
import java.io.*;
 
 /** 
 * Eredita da JPanel e rappresenta l'area di lavoro del grafo.
 * 
 * @author Massimiliano Ferretti matricola 79769 
 */

 public class Graph 
  implements Serializable
  {
  private Arc arcInProgress;
  private int currentNodeIndex;
  private List<Node> nodes;
  private List<Arc> arcs;
  private boolean showHelp;
  
  private Point mousePos;
  
  /**
   *  Restituisce un array di tutti i Nodi presenti nel grafo
   *  @return array di Node
   *  @see Node      
   */      
  public Node[] getNodes()
  {
    return nodes.toArray(new Node[nodes.size()]);
  }
  
  /**
   *  Restituisce un array di tutti gli archi presenti nel grafo
   *  @return array di Arc
   *  @see Arc      
   */      
  public Arc[] getArcs()
  {
    return arcs.toArray(new Arc[arcs.size()]);
  }     
  
  /**
   * Cambia lo stato di visualizzazione dell'help.
   */     
  public void toggleShowHelp()
  {
    showHelp = !showHelp;
  }
 
  /**
   * Costruttore di default
   */
  public Graph()
  {
    showHelp = true;
    nodes = new ArrayList<Node>();
    arcs = new ArrayList<Arc>();
    
    arcInProgress = null;
    currentNodeIndex = 0;
    
    mousePos = new Point();
  }     
  
  /**
   * Restituisce il Nodo correntemente selezionato
   * @return Il Nodo selezionato   
   */     
  public Node getSelectedNode()
  {
    for(int index=0; index<nodes.size(); index ++)
      if(nodes.get(index).isSelected())
        return nodes.get(index);
        
    return null;
  }
  
  /**
   * Restituisce il Nodo correntemente pre-selezionato
   * @return Il Nodo pre-selezionato   
   */     
  public Node getPreSelectedNode()
  {
    for(int index=0; index<nodes.size(); index ++)
      if(nodes.get(index).isPreSelected())
        return nodes.get(index);
        
    return null;
  }  

  /**
   * Restituisce l'Arco correntemente selezionato
   * @return L'Arco selezionato   
   */     
  public Arc getSelectedArc()
  {
    for(int index=0; index<arcs.size(); index ++)
      if(arcs.get(index).isSelected())
        return arcs.get(index);
        
    return null;
  }

  /**
   * Rimuove l'elemento correntemente selezionato.
   * L'elemento selezionato pu&ograve; essere un Nodo oppure un Arco.
   * Se nessun elemento &egrave; correntemente selezionato non fa nulla.   
   */          
  public void removeSelected()
  {
    Node selectedNode = getSelectedNode();
    if(selectedNode != null)
    {
      // Rimuove prima tutti gli Archi che fanno riferimento al nodo
      // Prepara la lista di archi da rimuovere
      List<Arc> arcToRemove = new ArrayList<Arc>();
      for(Arc arc : arcs)
        if( (arc.getSource().getIndex() == selectedNode.getIndex()) ||
            (arc.getTarget() != null && arc.getTarget().getIndex() == selectedNode.getIndex()) )
          arcToRemove.add(arc);      
      // Elimina gli archi marcati
      for(Arc arc : arcToRemove)
        arcs.remove(arc);
      arcToRemove.clear();
      
      nodes.remove(selectedNode);
    }
      
    Arc selectedArc = getSelectedArc();
    if(selectedArc != null)
      arcs.remove(selectedArc);
  }
  
  /**
   * Imposta le coordinate del mouse, servono per calcolare il delta dello
   * spostamento durante le operazioni di dragging e come elemento informativo.   
   * @param pos Le coordinate del mouse   
   */     
  public void setMousePos(Point pos)
  {
    mousePos.x = pos.x;
    mousePos.y = pos.y;
  }   
  
  /**
   * Seleziona il Nodo o l'Arco alle coordinate specificate.
   * @param pos Coordinate del punto di selezione
   */        
  public void select(Point pos)
  {
    // Cicla i nodi presenti per tentare la selezione
    for(int index=0; index<nodes.size(); index++)
      nodes.get(index).select(pos.x, pos.y);
      
    // Cicla gli archi presenti per tentare la selezione
    for(int index=0; index<arcs.size(); index++)
      arcs.get(index).select(pos.x, pos.y);
  }

  /**
   * Pre-seleziona il Nodo o l'Arco alle coordinate specificate.
   * @param pos Coordinate del punto di pre-selezione
   */        
  public void preSelect(Point pos)
  {
    // Cicla i nodi presenti per tentare la selezione
    for(int index=0; index<nodes.size(); index++)
      nodes.get(index).preSelect(pos.x, pos.y);
      
    // Cicla gli archi presenti per tentare la selezione
    for(int index=0; index<arcs.size(); index++)
      arcs.get(index).preSelect(pos.x, pos.y);
  }
  
  /**
   * Aggiorna il Nodo o l'Arco alle coordinate specificate.
   * @param pos Coordinate del punto di pre-selezione
   */        
  public void update(Point pos)
  {
    // Ottiene il delta dalla posizione precedente
    Point delta = new Point(pos.x - mousePos.x, pos.y - mousePos.y);
    setMousePos(pos);
    
    // Cicla i nodi presenti per tentare la selezione
    for(int index=0; index<nodes.size(); index++)
      nodes.get(index).update(delta.x, delta.y);
  }  
  
  /**
   * Incrementa di un'unit&agrave; il peso dell'arco correntemente selezionato
   * Se nessun arco &egrave; selezionato, non fa nulla.
   */        
   public void increaseCurrentArcWeight()
   {
    Arc selectedArc = getSelectedArc();
    if(selectedArc != null)
      {
        int w = selectedArc.getWeight() + 1;
        selectedArc.setWeight(w);
      }
   }

  /**
   * Decrementa di un'unit&agrave; il peso dell'arco correntemente selezionato
   * Se nessun arco &egrave; selezionato, non fa nulla.
   */        
   public void decreaseCurrentArcWeight()
   {
    Arc selectedArc = getSelectedArc();
    if(selectedArc != null)
      {
        int w = selectedArc.getWeight() - 1;
        selectedArc.setWeight(w);
      }
   }

  
  /**
   * Inizia un'operazione di creazione Arco, prendendo come origine il Nodo
   * correntemente selezionato.   
   * @param pos Coordinate del punto nel quale inizia l'operazione
   */
  public void beginArcCreation(Point pos)
  {
      // Se non esiste ancora un arco in creazione, lo crea
    if(arcInProgress == null)
    {
      Node selectedNode = getSelectedNode();
      
      // Se nessun Nodo &egrave;  correntemente selezionato, non ha senso
      // iniziare un arco
      if(selectedNode != null)
      {
        arcInProgress = new Arc(selectedNode);
        arcs.add(arcInProgress);
      }
    }
  }        
  
  /**
   * Durante un'operazione di creazione Arco, aggiorna le coordinate del target
   * fittizio.   
   * Se chiamato al di fuori delle chiamate beginArcCreation e endArcCreation
   * non ha alcun effetto.      
   * @param pos Coordinate del target fittizio
   * @see Graph#beginArcCreation
   * @see Graph#endArcCreation      
   */
  public void updateArcCreation(Point pos)
  {
    // Se abbiamo un arco in creazione, ne aggiorna le coordinate a prescindere
    // dai modificatori
    if(arcInProgress != null)
      arcInProgress.setTarget(pos);
  }        
  
    /**
   * Termina l'operazione di creazione di un Arco
   * @param pos Coordinate del punto nel quale termina l'operazione dell'arco
   */        
  public void endArcCreation(Point pos)
  {
    // Se stiamo rilasciando il mouse su di un Nodo, e abbiamo un Arco 
    // in creazione terminiamo l'arco
    Node preSelectedNode = getPreSelectedNode();
    
    if(arcInProgress != null && 
       preSelectedNode != null && 
       arcInProgress.getSource().getIndex() != preSelectedNode.getIndex())
        arcInProgress.setTarget(preSelectedNode);
      else
        arcs.remove(arcInProgress);
      
    arcInProgress = null;
  }
  
  /**
   * Aggiunge un Nodo al grafo.
   * @param pos Coordinate del punto nel quale creare il Nodo
   */
  public void addNode(Point pos)
  {
    currentNodeIndex++;
    Node newNode = new Node(currentNodeIndex, pos.x, pos.y);
    
    nodes.add(newNode);
  }
  
  /**
   * Riporta lo stato alla condizione iniziale
   */     
  public void reset()
  {
    nodes.clear();
    arcs.clear();
    
    arcInProgress = null;
    currentNodeIndex = 0;    
  }
  
  /**
   * Salva lo stato attuale del grafo sul file specificato
   * @param file Nome del file sul quale salvare il grafo   
   */     
  public void save(File file)
  {
    try {
      FileOutputStream fstream = new FileOutputStream(file);
      ObjectOutputStream ostream = new ObjectOutputStream(fstream);
      
      ostream.writeObject(this);
      ostream.close();
      fstream.close();
    } catch(IOException e)
    {
      System.out.println(e);
    }
  }
  
  /**
   * Carica un grafo da un file.
   * @param file Nome del file dal quale caricare il grafo
   * @return L'oggetto Graph caricato      
   */     
  public static Graph load(File file)
  {
    try {
      FileInputStream fstream = new FileInputStream(file);
      ObjectInputStream ostream = new ObjectInputStream(fstream);
      Graph result = (Graph) ostream.readObject();
      ostream.close();
      fstream.close();
      return result;
    } catch(IOException e)
    {
      System.out.println(e);
      return null;
    } catch(ClassNotFoundException e)
    {
      System.out.println(e);
      return null;
    }
  }                 
  
  private void paintBackground(Graphics2D g2d, int w, int h)
  {
    final int spaceBetweenDots = 15;
    final int dotSize = 1;
  
//    g2d.setColor(Color.YELLOW);
//    g2d.fillRect(0, 0, getWidth(), getHeight());

    g2d.setColor(Color.BLACK);    
    for(int x=spaceBetweenDots; x<w; x+=spaceBetweenDots)
      for(int y=spaceBetweenDots; y<h; y+=spaceBetweenDots)
        g2d.fillOval(x, y, dotSize, dotSize);
  }
  
  private void paintHelp(Graphics2D g, int w, int h)
  {
    Font fontTitle = new Font("Verdana", Font.BOLD, 12);
    Font fontText =  new Font("Verdana", Font.ITALIC, 10);
    final int padding = 15;
    
    String title = "GraphPath";
    List<String> helpRows = new ArrayList<String>();
    
    helpRows.add("LMB - Seleziona");
    helpRows.add("RMB - Crea nodo");
    helpRows.add("Shift+LMB - Crea arco");
    helpRows.add("Ctrl+N - nuovo grafo");
    helpRows.add("Ctrl+O - carica un grafo");
    helpRows.add("Ctrl+S - salva il grafo");
    helpRows.add("Q - incrementa peso arco");
    helpRows.add("A - decrementa peso arco");
    helpRows.add("D - esegui Dijkstra");
    helpRows.add("B - esegui Bellman-Ford");
    helpRows.add("TAB - Mostra/Nascondi guida");

    String longestString = "";
    for(String row : helpRows)
      if(row.length() > longestString.length())
        longestString = row;
    
    FontMetrics metrics = g.getFontMetrics(fontText);
    int mh = metrics.getHeight();
    int mw = metrics.stringWidth(longestString);
    g.setColor(Color.BLACK);
    
    g.setFont(fontTitle);
    Point pos = new Point(w - padding - mw, padding);
    g.drawString(title, pos.x, pos.y);
    
    g.setFont(fontText);
    for(int index=0; index < helpRows.size(); index++)
      g.drawString(helpRows.get(index), pos.x, pos.y + mh*(index+1)); 
  }
  
  /**
   * Disegna l'area del grafo, partendo dallo sfondo e disegnando poi tutti i 
   * nodi e gli archi memorizzati. Inoltre, se showHelp &egrave; true disegna un box
   * informativo con tutti i comandi disponibili.     
   * @param g Contesto grafico, tipicamente passato da un evento paint di un JPanel
   * @param w Dimensioni orizzontali del canvas
   * @param h Dimensioni verticali del canvas          
   */     
  public void paint(Graphics2D g, int w, int h)
  {
    // Salva le impostazioni
    Color defaultColor = g.getColor();
    Stroke defaultStroke = g.getStroke();
    Font defaultFont = g.getFont();
    
    paintBackground(g, w, h);
    
    int index=0;
    // Disegna i nodi
    for(index=0; index<nodes.size(); index++)
      nodes.get(index).paint(g);
    // Disegna gli archi
    for(index=0; index<arcs.size(); index++)
      arcs.get(index).paint(g);
    
    // Disegna le coordinate
    g.setColor(Color.BLACK);    
    g.drawString("(" + mousePos.x + ";" + mousePos.y + ")", 5, h - 15);
    
    // Disegna il box informativo
    if(showHelp)
      paintHelp(g, w, h);
      
    // Reimposta le impostazioni originali
    g.setFont(defaultFont);
    g.setStroke(defaultStroke);
    g.setColor(defaultColor);      
  }                            
 }
